using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using SolarWinds.Helpers;

namespace SolarWinds.Effects
{
    /// <summary>
    /// The starfield that renders behind the game, including a parallax effect.
    /// </summary>
    public class Starfield2D : DrawableGameComponent
    {
        #region Constants

        /// <summary>
        /// The number of stars in the starfield.
        /// </summary>
        private const int cNumberOfStars = 256;

        /// <summary>
        /// The number of layers in the starfield.
        /// </summary>
        private const int cNumberOfLayers = 8;

        /// <summary>
        /// The colors for each layer of stars.
        /// </summary>
        static readonly Color[] sLayerColors = new Color[cNumberOfLayers] 
                                                  { 
                                                      new Color(255, 255, 255, 255), 
                                                      new Color(255, 255, 255, 216), 
                                                      new Color(255, 255, 255, 192), 
                                                      new Color(255, 255, 255, 160), 
                                                      new Color(255, 255, 255, 128), 
                                                      new Color(255, 255, 255, 96), 
                                                      new Color(255, 255, 255, 64), 
                                                      new Color(255, 255, 255, 32) 
                                                  };

        /// <summary>
        /// The movement factor for each layer of stars, used in the parallax effect.
        /// </summary>
        static readonly float[] sMovementFactors = new float[cNumberOfLayers]
                                                      {
                                                          0.9f, 0.8f, 0.7f, 0.6f, 0.5f, 0.4f, 0.3f, 0.2f
                                                      };

        /// <summary>
        /// The maximum amount of movement allowed per update.
        /// </summary>
        /// <remarks>
        /// Any per-update movement values that exceed this will trigger a 
        /// starfield reset.
        /// </remarks>
        private const float cMaximumMovementPerUpdate = 128f;

        /// <summary>
        /// The background color of the starfield.
        /// </summary>
        private static readonly Color sBackgroundColor = new Color(0, 0, 32);

        /// <summary>
        /// The size of each star, in pixels.
        /// </summary>
        private const int cStarSize = 2;

        #endregion

        #region Gameplay Data

        /// <summary>
        /// The last position, used for the parallax effect.
        /// </summary>
        private Vector2 mLastPosition = Vector2.Zero;

        private Vector2 mPosition = Vector2.Zero;

        public Vector2 Position
        {
            get { return mPosition; }
            set
            {
                if (mPosition != value)
                {
                    mLastPosition = mPosition;
                    mPosition = value;
                }
            }
        }

        /// <summary>
        /// The stars in the starfield.
        /// </summary>
        private readonly Vector2[] mStars;


        #endregion

        #region Graphics Data

        /// <summary>
        /// The SpriteBatch used to render the starfield.
        /// </summary>
        private SpriteBatch mSpriteBatch;

        /// <summary>
        /// The texture used for each star, typically one white pixel.
        /// </summary>
        private Texture2D mStarTexture;

        /// <summary>
        /// The cloud texture that appears behind the stars.
        /// </summary>
        private Texture2D mCloudTexture;

        /// <summary>
        /// The effect used to draw the clouds.
        /// </summary>
        private Effect mCloudEffect;

        /// <summary>
        /// The parameter on the cloud effect that receives the current position
        /// </summary>
        private EffectParameter mCloudEffectPosition;

        private StateBlock mStateBlock;

        #endregion


        #region Initialization Methods

        /// <summary>
        /// Create a new Starfield object.
        /// </summary>
        /// <param name="game">The game instance</param>
        public Starfield2D(Game game)
            :base(game)
        {
            // initialize the stars
            mStars = new Vector2[cNumberOfStars];
        }

        #region Load / Unload content

        /// <summary>
        /// Load graphics data from the system.
        /// </summary>
        protected override void LoadContent()
        {
            base.LoadContent();

            // load the cloud texture
            mCloudTexture = Game.Content.Load<Texture2D>("Textures/Clouds");

            // load the cloud effect
            mCloudEffect = Game.Content.Load<Effect>("Shaders/Clouds");
            mCloudEffectPosition = mCloudEffect.Parameters["Position"];

            // create the star texture
            mStarTexture = new Texture2D(GraphicsDevice, 
                                         1, 1, 1, TextureUsage.None, SurfaceFormat.Color);
            mStarTexture.SetData(new Color[] { Color.White });

            // create the SpriteBatch object
            mSpriteBatch = new SpriteBatch(GraphicsDevice);

            mStateBlock = new StateBlock(GraphicsDevice);

            Reset(mPosition);
        }

        /// <summary>
        /// Release graphics data.
        /// </summary>
        protected override void UnloadContent()
        {
            mCloudTexture = null;
            mCloudEffect = null;
            mCloudEffectPosition = null;

            if (mStarTexture != null)
            {
                mStarTexture.Dispose();
                mStarTexture = null;
            }

            if (mSpriteBatch != null)
            {
                mSpriteBatch.Dispose();
                mSpriteBatch = null;
            }
        }

        #endregion

        /// <summary>
        /// Reset the stars and the parallax effect.
        /// </summary>
        /// <param name="position">The new origin point for the parallax effect.</param>
        public void Reset(Vector2 position)
        {
            // recreate the stars
            int viewportWidth = GraphicsDevice.Viewport.Width;
            int viewportHeight = GraphicsDevice.Viewport.Height;
            for (int i = 0; i < mStars.Length; ++i)
            {
                mStars[i] = new Vector2(RandomHelper.Random.Next(0, viewportWidth),
                                        RandomHelper.Random.Next(0, viewportHeight));
            }

            // reset the position
            mLastPosition = mPosition = position;
        }


        #endregion

        public override void Update (GameTime gameTime)
        {
            // determine the movement vector of the stars
            // -- for the purposes of the parallax effect, 
            //    this is the opposite direction as the position movement.
            Vector2 movement = -1.0f * (mPosition - mLastPosition);

            // if we've moved too far, then reset, as the stars will be moving too fast
            if (movement.Length() > cMaximumMovementPerUpdate)
            {
                Reset(mPosition);
                return;
            }

            // create a rectangle representing the screen dimensions of the starfield
            var starfieldRectangle = new Rectangle(0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height);

            // move all of the stars
            for (int i = 0; i < mStars.Length; i++)
            {
                // move the star based on the depth
                int depth = i % sMovementFactors.Length;
                mStars[i] += movement * sMovementFactors[depth];

                // wrap the stars around
                if (mStars[i].X < starfieldRectangle.X)
                {
                    mStars[i].X = starfieldRectangle.X + starfieldRectangle.Width;
                    mStars[i].Y = starfieldRectangle.Y +
                                  RandomHelper.Random.Next(starfieldRectangle.Height);
                }
                if (mStars[i].X > (starfieldRectangle.X + starfieldRectangle.Width))
                {
                    mStars[i].X = starfieldRectangle.X;
                    mStars[i].Y = starfieldRectangle.Y +
                                  RandomHelper.Random.Next(starfieldRectangle.Height);
                }
                if (mStars[i].Y < starfieldRectangle.Y)
                {
                    mStars[i].X = starfieldRectangle.X +
                                  RandomHelper.Random.Next(starfieldRectangle.Width);
                    mStars[i].Y = starfieldRectangle.Y + starfieldRectangle.Height;
                }
                if (mStars[i].Y >
                    (starfieldRectangle.Y + GraphicsDevice.Viewport.Height))
                {
                    mStars[i].X = starfieldRectangle.X +
                                  RandomHelper.Random.Next(starfieldRectangle.Width);
                    mStars[i].Y = starfieldRectangle.Y;
                }
            }


        }

        #region Draw Methods

        /// <summary>
        /// Draw the starfield.
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            mStateBlock.Capture();

            // create a rectangle representing the screen dimensions of the starfield
            var starfieldRectangle = new Rectangle(0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height);
            
            // draw a background color for the starfield
            mSpriteBatch.Begin(SpriteBlendMode.None);
            mSpriteBatch.Draw(mStarTexture, starfieldRectangle, sBackgroundColor);
            mSpriteBatch.End();

            // draw the cloud texture
            mSpriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate,
                               SaveStateMode.None);
            mCloudEffect.Begin();
            mCloudEffectPosition.SetValue(mPosition);
            mCloudEffect.CurrentTechnique.Passes[0].Begin();
            mSpriteBatch.Draw(mCloudTexture, starfieldRectangle, null, Color.White, 0.0f,
                              Vector2.Zero, SpriteEffects.None, 1.0f);
            mCloudEffect.CurrentTechnique.Passes[0].End();
            mCloudEffect.End();
            mSpriteBatch.End();
            
            // draw all of the stars
            mSpriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred, SaveStateMode.SaveState);
            for (int i = 0; i < mStars.Length; i++)
            {
                // draw the star
                int depth = i % sMovementFactors.Length;
                mSpriteBatch.Draw(mStarTexture,
                                  new Rectangle((int)mStars[i].X, (int)mStars[i].Y, cStarSize, cStarSize),
                                  null, sLayerColors[depth]);
            }
            mSpriteBatch.End();

            mStateBlock.Apply();
        }


        #endregion

        /// <summary>
        /// Disposes this object.
        /// </summary>
        /// <param name="disposing">
        /// True if this method was called as part of the Dispose method.
        /// </param>
        protected override void  Dispose(bool disposing)
        {
            if (disposing)
            {
                lock (this)
                {
                    if (mStarTexture != null)
                    {
                        mStarTexture.Dispose();
                        mStarTexture = null;
                    }
                    if (mSpriteBatch != null)
                    {
                        mSpriteBatch.Dispose();
                        mSpriteBatch = null;
                    }
                }
            }

            base.Dispose(disposing);
        }
    }
}